# Configuration, Environments and Secrets
# Configuration
A way to easily find and change the behavior of a program based on different intentions by changing the values of appropriate variables.
For example:
- Finding and changing the number of threads used by a program.
- Finding and changing the database URL used by a program.
So, configuration should be:
- Composed of variables and their values.
- Easy to find.
- Easy to change.
# Environments
A collection of values for different configuration variables defining a particular intention.
For example:
- A development environment containing a set of values for database URL, number of threads etc. that are tuned for serving the developer.
- A production environment containing a set of values for database URL, number of threads etc. that are tuned for serving the users.
So, environments should be:
- Composed of values for different configuration variables, including secrets.
- Properly isolated and protected.
- Easy to provision, but not very easy to change, as changing one value means changing the entire environment i.e. changing the intention.
# Secrets
Values that are not meant to be shared with the world.
For example:
- Database URL.
So, secrets should be:
- Encrypted.
# Combining All 3
- Configuration variables should be public and easy to find i.e. defined in the codebase of the program as a single source of truth, i.e. one single file.
- Values for configuration variables should either be public (defined in the codebase), or should come from the environment.
- Values that are defined in the codebase should not contain plain text secrets, but may contain encrypted secrets.
- Values that are defined in the environment should preferebly serve as switches, and may contain secrets.
- Environments should be encrypted, isolated and protected as they may contain secrets and access to critical services.
# Proof of Concept
- For each program, create a file containing all the configuration variables related to that particular program.
- The value of each variable should be either defined directly, or defined conditionally based on the environment.
Example:
See https://github.com/sayanarijit/envanumber_of_threads = 10 database_url = match env: case "dev": "dev_url" case "prod": environment.decrypt("prod_url") else: "default_url"copy success - Secret values or master password should come from the environment.
- If there is a common configuration defined separately in a common repo, only import the values, and define the variables explicitly for each program. So that it's easy to find.
- Define the environment specific values in the environment variables (/etc/environment). They are easy to provision, but requires reboot to change.
- Define secrets in git synced vault, with the master password defined as environment variable, or as a restricted file in user's home directory.
- Harden security of each environment.
# Python implementation
# settings.py import os from pathlib import Path from typing import Callable, TypeVar T = TypeVar("T") def env(**kwargs: T | Callable[[], T]) -> T: val = kwargs[ENVIRONMENT] return val() if callable(val) else val def readfile(path: str | Path, type_: Callable[[str], T] = str) -> Callable[[], T]: return lambda: type_(Path(path).read_text().strip()) ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev") DATABASE_URL = env( dev="dev_url", prod=readfile("/home/user/secrets/database_url"), )copy success
# My new TOML based generic implementation in Rust
Tool: http://settingspec.arijitbasu.in/
[spec] # Optional: Declare the specification here profile.key = "SETTINGSPEC_PROFILE" # Default: Environment variable name to switch between profiles profile.options = [ # Optional: Enables strict checking of per-profile declarations "dev", "stage", "prod", ] profile.default = "dev" # Optional: Default profile when the switch is not set [settings] # Declare the settings here with syntax: `<key>._.<profile>.<directive> = <value>` key1._.default.val = "val1" # Default value for all profiles key1._.prod.val = "prod1" # Override the default value when SETTINGSPEC_PROFILE=prod key2._ = { # Strictly define different values for different profiles dev.val = "dev2", stage.val = "stage2", prod.val = "prod2", }copy success